package cz.cuni.amis.pogamut.episodic.episodes;

import cz.cuni.amis.pogamut.episodic.decisions.Node;
import cz.cuni.amis.pogamut.episodic.memory.Affordance;
import cz.cuni.amis.pogamut.episodic.memory.AgentMemory;
import cz.cuni.amis.pogamut.episodic.memory.IdGenerator;
import cz.cuni.amis.pogamut.episodic.schemas.SchemaEpisodeNode;
import cz.cuni.amis.pogamut.episodic.schemas.SchemaSlot;
import cz.cuni.amis.pogamut.episodic.schemas.SlotContent;
import cz.cuni.amis.pogamut.episodic.visualizer.VisualizationEvent;
import cz.cuni.amis.pogamut.episodic.visualizer.VisualizationEventType;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.Queue;
import java.util.Set;
import java.util.Stack;

/**
 * The <code>Episode</code> class represents one episode experienced
 * by an agent. It can be pictured as customized tree structure
 * consisting of <code>EpisodeNode</code>s and more. It has a root node
 * and that represents top-level goal. Its child is actions
 * performed to achieve that goal, its children are goals needed
 * to perform that action etc. In the leaves of the tree are nodes
 * representing atomic actions. Moreover actions and intentions can
 * have several affordance slots that need to be filled by applicable
 * <code>ObjectNode</code>s.
 * <p>
 * Episode is open until it dissappears from agent's short-term memory.
 * Once it is finished no nodes should be added into it, nodes should
 * only be forgotten.
 *
 * @author Michal Cermak
 */
public class Episode implements Serializable {
    /**
     * Determines if a de-serialized file is compatible with this class.
     *
     * Maintainers must change this value if and only if the new version
     * of this class is not compatible with old versions. See Sun docs
     * for <a href=http://java.sun.com/products/jdk/1.1/docs/guide
     * /serialization/spec/version.doc.html> details. </a>
     *
     * Not necessary to include in first version of the class, but
     * included here as a reminder of its importance.
     */
    private static final long serialVersionUID = 1L;

    /** The <code>EpisodeNode</code> representing top-level goal
     * of this episode (root of episode tree).
     */
    public EpisodeNode root = null;

    /** Reference to the <code>Chronobag</code> this episode belongs to.
     * The age of an episode has to be deduced from the age of its
     * parent chronobag.
     */
    private Chronobag parentChronobag = null;

    /** An indicator whether the episode was finished. All episodes are created
     * as not finished. It is finished when its top-level goal is finished.
     * <p>
     * Episode can be transferred into <code>finishedEpisodes</code> list
     * in a chronobag even when it is not finished. This happens to all
     * episodes that had to be interrupted because agent went to sleep
     * at the end of a day. But once it is finished, it cannot appear in
     * the <code>currentEpisodes</code> list in a chronobag.
     */
    public boolean finished = false;

    /**
     * <strong>This ID is different than other node IDs in memory.</strong>
     * This is not used for visualize purposes. It is used to determine when
     * two episodes in different chronobags are copies of one episode and when
     * they are both unique episodes.
     */
    public final int idEpisode;

    /**
     * A reference to ID generator structure, that will provide
     * the unique IDs to all inside node. This unique IDs are mostly
     * needed when identifying the visualized vertices (because nodes
     * can have identical names).
     */
    public final IdGenerator idGen;

    /**
     * A score of an episode that is used to determine whether it should
     * be copied to more abstract chronobag. Initial value is 0 and it is
     * relalculated each night. Once the value passes given threshold, episode
     * copy is created and added to more abstract chronobag. Episode can
     * be copied only once.
     */
    private double copyScore = 0;

    /**
     * Indicator telling if the episode was already copied to more abstract
     * chronobag. Each episode can be copied only once.
     */
    private boolean copied = false;

    /**
     * When last of episode nodes (root) of this episode is deleted. It sets
     * this flag to true and it means whole episode should be deleted.
     */
    public boolean deleted = false;

    /**
     * The <code>next</code> field links <code>Episode<code>s of a chronobag
     * in order they were executed. It contains episodes executed
     * just after current episode.
     * <p>
     * If episode was interrupted it can have more than one successor.
     * Episodes in next list are ordered in order they were executed -
     * first interruptions are at the beginning, final successor is at the end.
     */
    public ArrayList<Episode> next = new ArrayList<Episode>();

    /**
     * The <code>previous</code> field contains <code>Episode<code>s of
     * a chronobag that were executed just before this episode.
     * <p>
     * If episode was interrupted it can have more than one predecessor.
     * Episodes in next list are ordered in reverse order than they
     * were executed - first predecessors are at the end,
     * last interruption is at the beginning.
     */
    public ArrayList<Episode> previous = new ArrayList<Episode>();

    // Real age of this episode. Note that this must not be used for any computational purposes, as
    // it defeats the goal of the model.
    // It can be used for verification of results.
    public int realAge = 0;

    public final AgentMemory mem;

    /**
     * Instantiate the class by providing reference to a parent <code>Chronobag</code>.
     * <p>
     * Make sure episode is also added to the episodes of the parent chronobag -
     * to the <code>currentEpisodes</code> list.
     *
     * @param   parent reference to the parent <code>Chronobag</code> structure.
     */
    public Episode(Chronobag parent) {
        parentChronobag = parent;
        mem = parent.chrono.mem;
        idGen = parent.idGenerator;
        idEpisode = idGen.getNewId();
    }

    /**
     * Instantiate the class by providing reference to an already instantiated
     * <code>Episode</code>. This can be used to create episode copies with
     * identical episode IDs.
     * <p>
     * Make sure episode is also added to the episodes of the parent chronobag -
     * to the <code>currentEpisodes</code> list.
     *
     * @param   other reference to instantiated <code>Episode</code> structure.
     */
    public Episode(Episode other) {
        mem = other.mem;
        finished = other.finished;
        idGen = other.idGen;
        idEpisode = other.idEpisode;
    }

    /** This method should be called for current episode each time agent
     * executes an atomic action. It is the only way to add new
     * <code>EpisodeNode</code>s into the <code>Episode</code>.
     * If episode was just started it is responsible for creating
     * the root node. It then makes sure all trace nodes, node
     * representing current atomic action, slots and objects used are created.
     *
     * @param   newNodeName Name of an atomic action that was executed.
     * @param   trace   List of names of decision nodes that lead to
     * the atomic action executed. The top-level goal is first in the list and
     * parent of atomic action is last item in the list.
     * @param   affordances     List of <code>Affordance</code> objects.
     * Each object represent one filled affordance slot that was
     * needed for the execution of current atomic action.
     * @param   time            The time of execution of this node.
     */
    public boolean addNewNode(String newNodeName, ArrayList<String> trace, ArrayList<Affordance> affordances, long time) throws Exception {
        boolean updated = parentChronobag.chrono.mem.getDecisionTree().ensureNodes(newNodeName, trace, affordances, mem.getIdGenerator());
        if (updated) mem.fireVisualizationEvent(new VisualizationEvent(this, VisualizationEventType.DECISION_TREE_UPDATE, mem));

        if (root == null)  {
            Node associated = parentChronobag.chrono.mem.getDecisionTree().topLevelGoals.get(trace.get(0));
            root = new EpisodeNode(trace.get(0), null, this, associated);
            parentChronobag.numberOfEpisodeNodes++;
        }
        EpisodeNode node = root;
        if (!node.getName().equals(trace.get(0))) throw new RuntimeException("Root node not matching.");
        int index = 1;
        // Ensure the complete trace already exists.
        while (index <= trace.size()) {
            if (index == trace.size()) break;
            // This may fail only if the node already existed.
            if (node.addChildNode(trace.get(index), time));
            node = node.getChild(trace.get(index));
            index++;
        }
        // The newly added node is either Action or Intention, just process affordances.
        if (!newNodeName.equals(trace.get(trace.size()-1))) {
            // Add atomic action if not.
            node.addChildNode(newNodeName, time);
            node = node.getChild(newNodeName);
        }
        Affordance aff;
        for (int i = 0; i < affordances.size(); i++) {
            aff = affordances.get(i);
            node.addSlot(aff.type);
            if (aff.item != null) {
                if (!parentChronobag.objectNodes.containsKey(aff.item)) {
                    parentChronobag.createObjectNode(aff.item);
                }
                node.fillSlot(aff.type, aff.item);
            }
        }
        return false;
    }

    /** Marks specified episodic node as finished. If the root node of
     * an episode is marked as finished, whole episode is marked
     * as finished.
     *
     * @param   node    Name of a node that was finished.
     * @param   trace   Names of traces nodes that lead to the finished node,
     * beginning with top-level goal and ending with node's parent.
     * <strong>Cannot be empty</strong>.
     * @param   succeeded   True if node was finished successfully.
     * @return  Returns true node was successfully located and marked,
     * false otherwise.
     */
    public boolean finishNode(String node, ArrayList<String> _trace, boolean succeeded) throws Exception {
        ArrayList<String> trace = new ArrayList<String>(_trace); // Duplicate to make it immutable

        if (!trace.contains(node)) {
            // Assume that node is an atomic action, let's add it here to make things simple.
            trace.add(node);
        }
        // Remove the root, recursively remove everything else.
        boolean rv;
        if (node.equals(trace.get(0))) {
            trace.remove(0);
            rv = root.finishNode(node, trace, succeeded, true, 0);
            if (trace.isEmpty()) {
                // If after cleaning up the trace is empty. We just closed the episode.
                root.finished = true;
                root.succeeded = succeeded;
            }
        } else {
            rv = root.finishNode(node, trace, succeeded, false, 1);
        }
        if (root.finished) finished = true;
        return rv;
    }

    /** Getter method for the <code>parentChronobag</code> variable.
     *
     * @return Returns a reference to the <code>Chronobag</code>
     * this episode belongs to. The age of an episode has to be deduced
     * from the age of its parent chronobag.
     */
    public Chronobag getParentChronobag() {
        return parentChronobag;
    }

    /**
     * Sets the reference to parent chronobag unless it was set before.
     *
     * @param c Reference to the parent chronobag.
     * @return  Return true if new refernce was set, returns false if reference
     * was set before.
     */
    public boolean setParentChronobag(Chronobag c) {
        if (parentChronobag != null) return false;
        parentChronobag = c;
        return true;
    }

    /** Getter method for the <code>root</code> variable.
     *
     * @return Returns the <code>EpisodeNode</code> representing
     * top-level goal of this episode (root of episode tree).
     */
    public EpisodeNode getRoot() {
        return root;
    }

    /**
     * Calculates the similarity of two episodes. Episodes are more similar
     * when have more common nodes. In order to be identical (similarity = 1)
     * all episode nodes have to be the same and all slots have to be filled
     * with identical objects. If the top level goals are not identical,
     * the similarity is always 0.
     *
     * @param other Reference to the episode that should be compared with
     * the current one.
     * @return  Returns number between 0 and 1. 1 means episodes are identical.
     */
    public double episodeSimilarity(Episode other) {
        if (!root.getName().equals(other.root.getName())) {
            // Episodes with different roots are not similar at all.
            return 0;
        }

        int intersection = root.numberOfCommonSubNodesWithObjects(other.getRoot());
        int max = Math.max(root.numberOfSubNodesWithObjects + 1, other.getRoot().numberOfSubNodesWithObjects + 1);
        if (0 > intersection || intersection > max) {
            int i = 0;
            i++;
        }
        assert (0 <= intersection && intersection <= max);
        return intersection / max;
    }

    /**
     * Creates a copy of the episode structure. This method is called when episode
     * is suppossed to be copied to more abstract chronobag. All the episode
     * nodes are new unique IDs. Episode ID stays the same. Episode does not
     * have parent chronobag yet.
     *
     * @return  Returns a copy of this episode.
     */
    public Episode createCopy(Chronobag c, boolean temporary) {
        Episode e = new Episode(this);
        e.setParentChronobag(c);
        e.root = root.createCopy(null, e, temporary);
        root.validateNode(root);
        return e;
    }

    /**
     * Overloaded simplified copy.
     */
    public Episode createCopy(Chronobag c) {
        return createCopy(c, false);
    }

    /**
     * Sets the copied indicator to <code>True</code>
     *
     * @return  Return true if indicator was set to true now, returns false if
     * indicator was set to true before.
     */
    public boolean setCopied() {
        if (copied) return false;
        copied = true;
        return true;
    }

    /**
     * A getter method for the <code>copied</code> variable.
     *
     * @return  Returns an indicator telling if the episode was already
     * copied to more abstract chronobag. Each episode can be copied only once.
     */
    public boolean getCopied() {
        return copied;
    }

    /**
     * A getter method for the <code>copyScore</code> variable.
     *
     * @return  Returns a score of an episode that is used to determine
     * whether it should be copied to more abstract chronobag.
     * Once the value passes given threshold, episode copy is created and
     * added to more abstract chronobag.
     * <p>
     * Returns -1 if episode was already copied.
     */
    public double getCopyScore() {
        if (copied) return -1;
        return copyScore;
    }

    public double calculateCopyScore() {
        if (copied) return -1;

        int maxAttractiveness = 0;
        double avgAttractiveness = 0;
        int nodes = 0;
        Queue<EpisodeNode> q = new LinkedList<EpisodeNode>();
        q.add(root);
        EpisodeNode n;
        while (!q.isEmpty()) {
            n = q.poll();
            if (n.getAssociatedNode() != null) {
                avgAttractiveness += n.getAssociatedNode().getAttractivity();
                if (n.getAssociatedNode().getAttractivity() >  maxAttractiveness) {
                    maxAttractiveness = n.getAssociatedNode().getAttractivity();
                }
            }
            nodes++;
            for (EpisodeNode child : n.getChildrenNodes()) {
                q.add(child);
            }
        }
 //       assert (nodes == root.numberOfSubNodes + 1);
        avgAttractiveness = avgAttractiveness / nodes;
        copyScore = (maxAttractiveness + avgAttractiveness) / 2;

        return copyScore;
    }

    /**
     * Getter method for the episode id. Not used for visualization purposes.
     * Used to determine when two episodes were created as copies.
     *
     * @return  Returns ID of an episode.
     */
    public int getIdEpisode() {
        return idEpisode;
    }

    public boolean mergeWith(Episode other) {
        int removedNodes = root.numberOfSubNodes + 1 + other.root.numberOfSubNodes + 1;
        int removedNodesObjects = root.numberOfSubNodesWithObjects + 1 + other.root.numberOfSubNodesWithObjects + 1;

        assert (root.validateNode(root)) : root.getId();
        assert (root.validateNode(root.children.values())) : root.getId();
        assert (root.validateNode(other.root)) : root.getId();
        assert (root.validateNode(other.root.children.values())) : root.getId();
        assert (root.validateNode(root)) : root.getId();
        root = root.mergeWith(other.root);
        assert (root.validateNode(root)) : root.getId();
        assert (root.validateNode(root.children.values())) : root.getId();

        removedNodes -= root.numberOfSubNodes + 1;
        removedNodesObjects -= root.numberOfSubNodesWithObjects + 1;

        previous.addAll(other.previous);
        previous.remove(this);
        previous.remove(other);
        next.addAll(other.next);
        next.remove(this);
        next.remove(other);
        copied = copied && other.copied;
        other.deleted = true;
        finished = finished || other.finished;

        parentChronobag.episodesMerged(this, other, removedNodes, removedNodesObjects);
        return false;
    }

    /**
     * Attempt to derive the rest of the episode from schema.
     *
     * @param maxnodes Only consider schemas with this many additional nodes. Use -1 for any amount.
     * @return The newly derived episode
     */
    public Episode deriveEpisode(int level, int index, int maxnodes) {
        assert (root.validateNode(root)) : root.getId();
        assert (root.validateNode(root.children.values())) : root.getId();
        int nodesAdded = 0;

        //trace nodes
        Set<EpisodeNode> s = new HashSet<EpisodeNode>();
        s.add(root);
        EpisodeNode e, newNode;
        while (!s.isEmpty()) {
            e = s.iterator().next();
            s.remove(e);
            assert (e.validateNode(e));
            if (e.getParent() != null && e.associatedNode!= null
                    && !e.associatedNode.parent.getName().equals(e.getParent().associatedNode.getName())) {
                    newNode = e.deriveNodeTrace();
                    s.add(newNode);
                    s.removeAll(newNode.getChildrenNodes());
                    assert(e.validateNode(newNode));
                    e = newNode.getChild(e.getName());
            } else {
                assert (e.validateNode(e));
                for (EpisodeNode n : e.getChildrenNodes()) {
                    assert (e.validateNode(n));
                    s.add(n);
                }
            }
        }

        // Collect all nodes that currently contribute to this episode.
        Collection<SchemaEpisodeNode> schemaENodes = new ArrayList<SchemaEpisodeNode>();
        Collection<SlotContent> schemaContents = new ArrayList<SlotContent>();
        // Walk through the episode to collect info
        Stack<EpisodeNode> stack = new Stack<EpisodeNode>();
        SchemaSlot sSlot;
        EpisodeNode stackNode;
        stack.add(root);
        while (!stack.isEmpty()) {
            stackNode = stack.pop();
            if (stackNode.associatedNode == null) {
                continue;
            }
            schemaENodes.add(stackNode.associatedNode.getAssociatedNode());
            for (ObjectSlot slot : stackNode.getObjectSlots()) {
                if (slot.getType().equals("Other")) {
                    continue;
                }
                sSlot = slot.getParentNode().associatedNode.getAssociatedNode().getSlot(slot.getType());
                for (ObjectNode obj : slot.getUsedObjects()) {
                    schemaContents.add(sSlot.getSlotContent(obj.getName()));
                }
            }
            stack.addAll(stackNode.getChildrenNodes());
        }

        // Prepare a stack for walking through all the nodes.
        stackNode = root;
        while (stackNode != null) {
            if (stackNode.associatedNode == null) break;
            stack.push(stackNode);
            for (ObjectSlot slot : stackNode.getObjectSlots()) {
                if (slot.getType().equals("Other")) continue;
                sSlot = slot.getParentNode().associatedNode.getAssociatedNode().getSlot(slot.getType());
                for (ObjectNode obj : slot.getUsedObjects()) {
                    schemaContents.add(sSlot.getSlotContent(obj.getName()));
                }
            }
            stackNode = stackNode.getFirstChild().get(idEpisode);
        }

        EpisodeNode addedNode = null;
        EpisodeNode previousAddedNode = null;
        while(!stack.isEmpty() && ((maxnodes == -1) || (nodesAdded < maxnodes))) {
            stackNode = stack.peek();

            //add children until child is added or nothing more can be added
            previousAddedNode = addedNode;
            addedNode = stackNode.deriveChildEpisodicNode(level, index, schemaENodes, schemaContents);
            if (addedNode == null || addedNode == previousAddedNode) {
                stack.pop();
                for (ObjectSlot slot : stackNode.getObjectSlots()) {
                    if (slot.getType().equals("Other")) continue;
                    sSlot = slot.getParentNode().associatedNode.getAssociatedNode().getSlot(slot.getType());
                    for (ObjectNode obj : slot.getUsedObjects()) {
                        schemaContents.remove(sSlot.getSlotContent(obj.getName()));
                    }
                }
                EpisodeNode successor = stackNode.getSuccessor().get(idEpisode);
                if (successor != null && successor.associatedNode != null) {
                    stack.push(successor);
                    for (ObjectSlot slot : successor.getObjectSlots()) {
                        if (slot.getType().equals("Other")) continue;
                        sSlot = slot.getParentNode().associatedNode.getAssociatedNode().getSlot(slot.getType());
                        for (ObjectNode obj : slot.getUsedObjects()) {
                            schemaContents.add(sSlot.getSlotContent(obj.getName()));
                        }
                    }
                }
            } else {
                // Same as higher, this condition is just temporary
                if (addedNode.associatedNode != null) {
                    stack.push(addedNode);
                    schemaENodes.add(addedNode.associatedNode.getAssociatedNode());
                    for (ObjectSlot slot : addedNode.getObjectSlots()) {
                        sSlot = slot.getParentNode().associatedNode.getAssociatedNode().getSlot(slot.getType());
                        for (ObjectNode obj : slot.getUsedObjects()) {
                            schemaContents.add(sSlot.getSlotContent(obj.getName()));
                        }
                    }
                }
            }
        }

        assert (root.validateNode(root)) : root.getId();
        assert (root.validateNode(root.children.values())) : root.getId();

        return this;
    }

    @Override
    public String toString() {
        String s = "" + idEpisode;
        if (root != null) {
            s += " " + root.getName();
        }
        return s;
    }

}
